High Availability NATパターン(re:Invent 2014版)を試してみた
こんにちは、眠気の向こう側の高揚感を大事にしたい、せーのです。 Private Subnetにインスタンスを立てる時にインターネットとの接続をするためにNATを立てますが、NATは単一障害点になるので、冗長化を図り、高可用性を保つようにします。 この高可用性を保つ構成が今まで色々考えられてきました。今回はre:Invent 2014にて紹介された最新のHigh Availability NATパターンをご紹介します。
今までのHA NATパターン
HA NATのキホンと言えばCDPにもなっているHigh Availability NATパターンになります。弊社のブログにも紹介されていますね。この形です。
ではメインで使っていたNATに障害が起きた時の切り替えは具体的にどうするのでしょう。一般的にはスクリプトを組んで仕込んでおきます。NAT1に向けているRoute TableとNAT2に向けているRoute Tableをあらかじめ用意しておき、障害時にSubnetにアタッチしているRoute Tableを切り替えることでNATの向き先を変えます。
#!/bin/bash export AWS_DEFAULT_REGION=ap-northeast-1 export AWS_DEFAULT_OUTPUT=text WATCH_CMD="/bin/ping -c 3 -w 3" LOGFILE=/var/log/nat-failover.log # set variables specific to the environment to run the script source $1 #check route table on target NAT rt_id=`aws ec2 describe-route-tables --filter "Name=association.subnet-id,Values=$APP_SUBNET_PRI" --query RouteTables[].RouteTableId --output text` if [ "$rt_id" = "$APP_ROUTE_TABLE_2ND" ] ; then echo `date --rfc-3339 ns` "[WARN] $APP_SUBNET_PRI already has the 2nd Route Table." >> $LOGFILE : else if $WATCH_CMD ${NAT_PRI[1]} > /dev/null 2>&1; then echo `date --rfc-3339 ns` "[INFO] health check OK." >> $LOGFILE : else #check other NAT's health if $WATCH_CMD ${NAT_2ND[1]} > /dev/null 2>&1; then echo `date --rfc-3339 ns` "[WARN] Application Route tables associations failover triggered." >> $LOGFILE # replace association between route tables and subnets for Application for rt_assoc_id in `aws ec2 describe-route-tables --filter "Name=route-table-id,Values=$APP_ROUTE_TABLE_PRI" --query RouteTables[].Associations[*].RouteTableAssociationId --output text` do echo `date --rfc-3339 ns` "[WARN] Replace association $rt_assoc_id to $APP_ROUTE_TABLE_2ND." >> $LOGFILE aws ec2 replace-route-table-association --association-id $rt_assoc_id --route-table-id $APP_ROUTE_TABLE_2ND > /dev/null 2>&1 done else echo `date --rfc-3339 ns` "[INFO] every NAT's health check NG." >> $LOGFILE : fi fi fi
このようなスクリプトをcron登録し、NATを監視、障害時の切り替えを行います。 これで一般的な可用性は保たれますが、cronは最小で1分単位になるのでそれより短い時間での検知となると厳しくなります。
High Availability for Amazon VPC NAT Instances
そこでその検知時間を短くするような方式がAmazonの記事に載っております。基本的な構成は今までのHA NATパターンと変わりません。変わるのは検知スクリプトになります。
#!/bin/sh # This script will monitor another NAT instance and take over its routes # if communication with the other instance fails # NAT instance variables # Other instance's IP to ping and route to grab if other node goes down NAT_ID= NAT_RT_ID= # My route to grab when I come back up My_RT_ID= # Specify the EC2 region that this will be running in (e.g. https://ec2.us-east-1.amazonaws.com) EC2_URL= # Health Check variables Num_Pings=3 Ping_Timeout=1 Wait_Between_Pings=2 Wait_for_Instance_Stop=60 Wait_for_Instance_Start=300 # Run aws-apitools-common.sh to set up default environment variables and to # leverage AWS security credentials provided by EC2 roles . /etc/profile.d/aws-apitools-common.sh # Determine the NAT instance private IP so we can ping the other NAT instance, take over # its route, and reboot it. Requires EC2 DescribeInstances, ReplaceRoute, and Start/RebootInstances # permissions. The following example EC2 Roles policy will authorize these commands: # { # "Statement": [ # { # "Action": [ # "ec2:DescribeInstances", # "ec2:CreateRoute", # "ec2:ReplaceRoute", # "ec2:StartInstances", # "ec2:StopInstances" # ], # "Effect": "Allow", # "Resource": "*" # } # ] # } # Get this instance's ID Instance_ID=`/usr/bin/curl --silent http://169.254.169.254/latest/meta-data/instance-id` # Get the other NAT instance's IP NAT_IP=`/opt/aws/bin/ec2-describe-instances $NAT_ID -U $EC2_URL | grep PRIVATEIPADDRESS -m 1 | awk '{print $2;}'` echo `date` "-- Starting NAT monitor" echo `date` "-- Adding this instance to $My_RT_ID default route on start" /opt/aws/bin/ec2-replace-route $My_RT_ID -r 0.0.0.0/0 -i $Instance_ID -U $EC2_URL # If replace-route failed, then the route might not exist and may need to be created instead if [ "$?" != "0" ]; then /opt/aws/bin/ec2-create-route $My_RT_ID -r 0.0.0.0/0 -i $Instance_ID -U $EC2_URL fi while [ . ]; do # Check health of other NAT instance pingresult=`ping -c $Num_Pings -W $Ping_Timeout $NAT_IP | grep time= | wc -l` # Check to see if any of the health checks succeeded, if not if [ "$pingresult" == "0" ]; then # Set HEALTHY variables to unhealthy (0) ROUTE_HEALTHY=0 NAT_HEALTHY=0 STOPPING_NAT=0 while [ "$NAT_HEALTHY" == "0" ]; do # NAT instance is unhealthy, loop while we try to fix it if [ "$ROUTE_HEALTHY" == "0" ]; then echo `date` "-- Other NAT heartbeat failed, taking over $NAT_RT_ID default route" /opt/aws/bin/ec2-replace-route $NAT_RT_ID -r 0.0.0.0/0 -i $Instance_ID -U $EC2_URL ROUTE_HEALTHY=1 fi # Check NAT state to see if we should stop it or start it again # This sample script works well with EC2 API tools version 1.6.12.2 2013-10-15. If you are using a different version and your script is stuck at NAT_STATE, please modify the script to "print $5;" instead of "print $4;". NAT_STATE=`/opt/aws/bin/ec2-describe-instances $NAT_ID -U $EC2_URL | grep INSTANCE | awk '{print $4;}'` if [ "$NAT_STATE" == "stopped" ]; then echo `date` "-- Other NAT instance stopped, starting it back up" /opt/aws/bin/ec2-start-instances $NAT_ID -U $EC2_URL NAT_HEALTHY=1 sleep $Wait_for_Instance_Start else if [ "$STOPPING_NAT" == "0" ]; then echo `date` "-- Other NAT instance $NAT_STATE, attempting to stop for reboot" /opt/aws/bin/ec2-stop-instances $NAT_ID -U $EC2_URL STOPPING_NAT=1 fi sleep $Wait_for_Instance_Stop fi done else sleep $Wait_Between_Pings fi done
今までのパターンは検知、切り替えをcronに登録していましたが、それを無限ループさせることによってcronの制限から解放されます。$Wait_Between_Pingsを調整することにより1分以下の検知単位にも対応できます。また今まではRoute Tableを用意しておいて切り替える、という形を取っていたのですが、これはRoute TableのDestinationのInstance IDを書き換えることでRoutingを切り替えているのも違いですね。 ですが、上2つに共通する問題として「固定値が多い」ということがあります。ネットワークやアプリケーション層が変わるとその度にスクリプトの変数を書き換えなくてはいけなく、汎用的ではありません。そこでre:Invent 2014では新しいHA NATが提案されました。
Advanced Dynamic network Automation approaches by re:Invent 2014
新しいHA NATが提案されたのはre:Invent 2014の「ARC401:Black-Belt Networking for the Cloud Ninja」というセッション。動画やスライドはここから[ARC401]で検索してください。 新しいパターンではNATをAuto Scalingにすることでコストダウンと可用性の確保をしています。 またRoute Table IDを決め打ちするのではなく、Route TableにTagをつけて、Tagをフィルタリングして該当するRoute Tableを見つけ出す形を取ることで汎用性が高まります。
障害の検知はAuto Scalingに任せます。障害があるとNATインスタンスはTerminateし、新しいNATが立ち上がります。その立ち上がり時のcloud-initでRoute Tableの切り替えを行います。スクリプトを見てみましょう。
#!/bin/bash INSTANCE_ID=`/usr/bin/curl --silent http://169.254.169.254/latest/meta-data/instance-id` AZ=`/usr/bin/curl --silent http://169.254.169.254/latest/meta-data/placement/availability-zone` REGION="${AZ%?}" MAC=`curl --silent http://169.254.169.254/latest/meta-data/network/interfaces/macs/` VPC_ID=`curl --silent http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC/vpc-id` ROUTE_TABLES=`aws ec2 describe-route-tables --region $REGION --output text --filters "Name=tag:NATAZ,Values=any,$AZ" | grep ROUTETABLES | awk '{print $2}'` # Parse through RouteTables that need to be modified for MY_RT_ID in $ROUTE_TABLES; do aws ec2 replace-route --route-table-id $MY_RT_ID --destination-cidr-block 0.0.0.0/0 --instance-id $INSTANCE_ID` --region $REGION done
スクリプトの内容もだいぶ変わりました。今まで固定値になっていたInstance ID, AZ, Region, VPC IDは全てmeta-dataから取ってくる形になっています。またroute tableは[NATAZ]タグが自身のNATの所属するAZ(またはany)になっているRoute Table IDをまとめて抽出、Route TableのDestination先を自分のInstance IDに書き換えます。固定しているIDが全くない為、どこのVPCでも内容を全く変えずに通用しますね。汎用性が高まりました。
それでは実際にやってみましょう。既にCloudFormationのテンプレートがあるので、それを入れてみます。
テンプレートを入れるとこのようなサブネットが出来上がります。10.0.0.0/24, 10.0.2.0/24というpublic subnetにNATをそれぞれ1台ずつ、10.0.1.0/24, 10.0.3.0/24のprivate subnetからそれぞれのNATに向けてRoute Tableが向けられています。 10.0.1.0/24のサブネットの中にEC2インスタンスを一台立て、ポートフォワードでSSHログインして外に向かって打ってみると
[ec2-user@ip-10-0-1-63 ~]$ curl http://www.google.com/ <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> <TITLE>302 Moved</TITLE></HEAD><BODY> <H1>302 Moved</H1> The document has moved <A HREF="http://www.google.co.jp/?gfe_rd=cr&ei=38l8VIX8BueN8QeD-oDYAg">here</A>. </BODY></HTML>
バッチリ繋がります。 Route Tableを見てみると
NATに向けてルーティングがされています。 ここでNATをTerminateさせてみます。
Auto Scalingになっているので、自動的に新しいNATが立ち上がります。
この新しいNATが立ち上がる際にcloud-initで上のスクリプトが走り、自動的にRoute Tableの切り替えが行われます。 改めてRoute Tableを見てみると
新しいNATにルーティングが向けられているのが確認できます。
まとめと改善点
いかがでしょうか、汎用的なNAT用のCloudFormationを用意しておくと構築時にとても便利ですね。 ですがこれにもまだ改善点があります。現在の構成ですと障害時に新しいNATが立ち上がる時に初めてルーティングの切り替えが行われます。NATとなるEC2インスタンスが立ち上がるのは小さいものでも2分〜5分はかかるので、その間NATには繋がらないことになります。上でせっかく検知時間を秒単位まで早めたので、ここでそれを諦めるのはもったいないですね。 アイデアとしてはLifeCycleHookを利用してTerminate前に切り替えてから落とすようにしたら、なんて考えています。実験しなきゃ。